module(..., package.seeall)
require("md5")
local json=require'cjson'
require'zsvc.appbase'
require'zdc.doc'
require'zdc.idutil'
require'zwsapi.util'
require'zwsapi.webnode'
require'zwsapi.auth.simple'
local zlib_loaded, zlib = pcall(require, "zlib")

local WSApp = {}
zsvc.appbase.MakeInhMT(WSApp)
local WSApp_mt = {__metatable = {}, __index = WSApp}

function MakeInhMT(ch)
	ch=ch or {}
	for _i,_j in pairs(WSApp) do
		ch[_i]=_j
	end
	return ch
end

function new(config, logger, childmt)
	local obj =zsvc.appbase.new(config,logger,childmt or WSApp_mt)
	return obj
end
local up_init=WSApp.Init
function WSApp:Init(config)
	up_init(self,config)
	self.auth_mod = require(config.AUTH_MODULE or "zwsapi.auth.simple")
	self.auth=self.auth_mod.new(self, config.AUTH_MODULE_PARAMS)
	
	local acl=self:GetDpl('@ws')._secugroups
	acl.all_users     = function(user) return true end
	acl.Authenticated = function(user) return user and #user > 0 end
	acl.Anonymous     = function(user) return user=='' end
	acl.all_actions   = function(action) return true end
	acl.Admin = function(user)
		return user and self.auth:Get_metadata(self,user, "is_admin") == "true"
	end

	local groups_mt = {
		__index = function(table, key)
			return function (user)
						 return user and self.auth:Get_metadata(self,user, "is_"..key) == "true"
					 end
		end
	}
	acl.is = setmetatable({}, groups_mt)
	acl.edit_and_save = {"save", "edit", "preview"}
	acl.show = {"show", "show_content", "cancel"}
	acl.history_and_diff = {"history", "diff"}
	acl.show_etc = {"show", "show_content", "cancel", "history", "diff"}
end
function WSApp:HandleRequest(request,respone)
	local dal=self.dal
	local user,nid,act=request.user,request.node,request.act
	if not user then user='' end
	--dal.AUTHUSER=user
	local node,msg=dal:GetNode(nid)

	if not act or #act ==0 then
		respone.ret={nil,'no act arg'}
		return respone
	else
		if act=='New' then
			if node then respone.ret={nil,'node has exist'};return respone end
			local pn,rest=zdc.idutil.GetParent(cmd)
			pn=dal:GetNode(pn)
			if not pn then respone.ret={nil,'parent node not exist'};return respone end
			node,msg=pn:__sure_child(rest)
			if not node then respone.ret={nil,msg};return respone end
		end
		if not node then
			request.redirect=dal:make_url(dal.config.HOME_PAGE)
			if dal:make_url(nid)==request.redirect then
				request.redirect=nil
			end
			respone.ret={nil,'node not exist'}
			return respone			
		end
		if node.__is_a_stub then
			local prototype
			-- One possibility is that the prototype was specified in the request
			if request.params._pto then
				prototype = request.params._pto
			else
				-- Another possibility is that the node matches a pattern in
				-- PROTOTYPE_PATTERNS
				local patterns = self.config.PROTOTYPE_PATTERNS or {}
				for pattern, pattern_prototype in pairs(patterns) do
					if request.node_name:find(pattern) then
						prototype = pattern_prototype;
						break
					end
				end
			end
			if prototype then
				node:__update_fields({_pto=prototype})
			end
		end
		if act=='_fetch' then
			respone.ret={zdc.nodeapi.fetch_node(user,dal,node,unpack(request.args or {}))}
		else
			local webnode=zwsapi.webnode.new(self,node);
			respone.ret={select(2,assert(zdc.nodeapi.callact(user,dal,webnode,act,request)))}
			return respone,webnode
		end
		return respone
	end
end

function WSApp:WSHandleRequest(request, response)
	if self.logger:chkLevel('DEBUG') then
		self.logger:debug("REQUEST:\n"..zdc.doc.str_print(request))
	end

	self.cookie_name = self.config.COOKIE_NAME.."_"..md5.sumhexa(self.config.BASE_URL)

	request = self:translate_request(request)
	if request.params.__reset_force then
		self.logger:debug("REQUEST RESET FORCE")
		self:ResetCache(true)
	end
	request.is_indexable = true
	local olduser=request.user
	-- Now that we know who the user is, set the authentication cookie.
	local cookie_value = (request.user or "").."|"..(request.auth_token or "")
	response:set_cookie(self.cookie_name, {value=cookie_value, path="/"})

	-- Check if the action function requested a redirect.
	if request.redirect then
		response.headers["Content-Type"] = content_type or "text/html"
		response.headers["Location"] = request.redirect
		response:write(content or request.redirect)
		return response
	end
		
	-- If a GET request uses a non-canonical node id, redirect to the right one.
	local canonical_id = self:dirify(request.node_name)
	if request.method=="GET" and request.node_name ~= canonical_id then
		local new_url = self:make_url(canonical_id, request.action, request.params)
		response.headers["Location"] = new_url
		response.status = 301 -- permanent redirect
		response.headers["Content-Type"] = "text/html"
		response:write(new_url)
		return response
	end
	request.act=request.action
	request.args=request.params
	if request.node_name:sub(1,6)=='@ws/j/' then
		request.node='@ws/j'
		request.usenode=request.node_name
		request.node_name=request.node_name:sub(7)
	elseif zdc.idutil.IsRoot('@ws',request.node_name) then
		request.node=request.node_name
		request.usenode=request.node_name
	else
		request.node='@ws/j'
		request.usenode= request.node_name
	end
	
	local content, content_type
	local ok,err,node=xpcall(function ()return self:HandleRequest(request,response)end,debug.traceback)
	if not ok then
		self:ResetCache()
		content, content_type="HandleRequest ERR:\n".. ((self.config.SHOW_STACK_TRACE and err)or''),'text/plain'
		self.logger:debug("HandleRequest ERR:\n"..err)
	else
		self:ResetCache()
		content, content_type=response.ret[1],response.ret[2]
	end
	if not olduser and request.user then
		response:set_cookie(self.cookie_name, {value=request.user.."|"..(request.auth_token or ""), path="/"})
	end
	-- Check if the action logged out the user.
	if not request.user then
		response:set_cookie(self.cookie_name, {value="", path="/"})
	end
	-- If we have any custom HTML headers, add them to the response
	if node then
		for header,value in pairs(node.headers) do
			response.headers[header] = value
		end

		-- If we ave any cookie values, add them to the response
		for name,value in pairs(node.cookies) do
			if value == false then
				response:delete_cookie(name)
			else
				response:set_cookie(name, {value = value, path = "/"})
			end
		end
	end

	-- Reset the node cache.
	--TODO:

	-- Check if the action function requested a redirect.
	if request.redirect then
		response.headers["Content-Type"] = content_type or "text/html"
		response.headers["Location"] = request.redirect
		response:write(content or request.redirect)
		return response
	end

	-- If we didn't redirect, let's prepare to return the content.
	assert(content,content_type)
	if type(content)~='string' then
		content=json.encode(content)
		--self.logger:debug("json encode:\n"..zdc.doc.str_print(content))
	end
	response.headers["Content-Type"] = content_type or "text/html"

	-- Set headers for caching.
	if node and node.http_cache_control and node.http_cache_control~="" then
		node.headers["Cache-Control"] = node.http_cache_control
	end
	if node and node.http_expires and node.http_expires~="" then
		node.headers["Expires"] = os.date("!%a, %d %b %Y %H:%M:%S GMT",
													 os.time()+3600*1000*tonumber(node.http_expires))
	end

	-- Gzip the content if we have a gzip module and the user-agent accepts.
	-- (Gzip compression code borrowed from Ignacio Burgeno)
	if self.config.USE_COMPRESSION and zlib_loaded
		and string.find(request.env["HTTP_ACCEPT_ENCODING"], "gzip") then
		--zlib.compress(string buffer [, int level] [, int method] [, int windowBits] [, int memLevel] [, int strategy])
		--that magic came from http://lua-users.org/lists/lua-l/2005-03/msg00221.html
		local gz_content = zlib.compress(content, 9, nil, 15 + 16)
		response.headers["Content-Encoding"] = "gzip"
		response.headers["Content-Length"] = #gz_content
		response.headers["Vary"] = "accept-encoding"
		response:write(gz_content)
	else -- Otherwise just write the content.
		response:write(content)
	end
	if self.logger:chkLevel('DEBUG') then
		self.logger:debug("RESPONSE:\n"..zdc.doc.str_print(response.headers))
	end
	return response
end

function WSApp:hash_field_name(field_name, token)
	assert(self.config.TOKEN_SALT, "TOKEN_SALT must be set")
	return "field_"..md5.sumhexa(field_name..token..self.config.TOKEN_SALT)
end
function WSApp:translate_request (request)
	if request.method=="POST" then
		request.params = request.POST or {}
		local par=request.params
		for k,v in pairs(request.GET or {}) do
			if par[k]==nil then par[k]=v end 
		end
	else
		request.params = request.GET or {}
	end

	-- For a post action we'll need to unhash the parameters first.  Note that
	-- we don't care if the action was actually submitted via get or post: if
	-- an idempotent request was sent via POST, that's ok.  Instead, we divide
	-- actions into two types: those that were submitted with a post token and
	-- those that were submitted without.  Requests submitted with a post token
	-- are allowed to make changes to the state of the wiki.  They get their
	-- fields unhashed.  This means that if an action is submitted with a post
	-- token but its fields are not hashed, it will be processed as if
	-- submitted with no arguments.
	if request.params.post_token then
		assert(request.params.post_fields)
		--self.logger:debug("handling post parameters")
		local new_params = {}
		for k,v in pairs(request.params) do
			if k:sub(0,7) == "action_" then
				new_params[k] = v
			end
		end
		for name in string.gmatch(request.params.post_fields, "[%a_]+") do 
			--self.logger:debug(name)
			local field = self:hash_field_name(name, request.params.post_token)
			new_params[name] = request.params[field] or ""
			--self.logger:debug(new_params[name])
		end
		new_params.p = request.params.p or request.GET.p
		new_params.post_token = request.params.post_token
		new_params.post_timestamp = request.params.post_timestamp
		request.params = new_params
	else
		if request.method=="POST" then
			request.params.p=request.params.p or request.GET.p
		end
	end
	
	-- break "p" parameter into node name and the action
	if request.params.p and request.params.p~="" then
		request.node_name, request.action = zwsapi.util.split(request.params.p, "%.")
	else
		local uri=request.doc_root .. request.path_info
		if request.path_info=='/' then
			request.node_name = self.config.HOME_PAGE
		else
			request.node_name,request.action = zwsapi.util.split(uri, "%.")
		end
	end
	
	request.node_name = request.node_name:gsub("^/", ""):gsub("/$", ""):gsub("//+","/") -- remove the trailing slash
	request.action = request.action or "show"

	-- now login/logout/register the user
	if request.params.logout then 
		request.user = nil
	elseif (request.params.user or ""):len() > 0 then
		request.user, request.auth_token = self.auth:Authenticate(self,request.params.user,request.params.password)
		if not request.user then
			request.auth_message = "INCORRECT_PASSWORD"
			-- TODO: I am unsure what the behavior here should be.. 
			request.node_name = self.config.LOGIN_NODE
		else
			if request.params.next then
				request.redirect = self:make_url(request.params.next)
			end

			--self.logger:debug(request.user..","..request.auth_token)
		end
	else
		local cookie = request.cookies[self.cookie_name] or ""
		local user_from_cookie, auth_token = zwsapi.util.split(cookie, "|")
		if user_from_cookie then
			request.user = self.auth:Validate_token(self,user_from_cookie, auth_token)
			if request.user then
				request.auth_token = auth_token
			end
		end
	end
	return request
end

-----------------------------------------------------------------------------
-- Turns text into something that can be used as a node name.
-----------------------------------------------------------------------------
function WSApp:dirify(text)
	-- Turns a string into something that can be used as a node name.
	return (self.config.DIRIFY_FN or zwsapi.util.dirify)(text)
end

-----------------------------------------------------------------------------
-- Makes a URL from a table of parameters.
--
-- @param node_name		a node id.
-- @param action			action/command as a string.
-- @param params			query parameters
-- @param anchor			link anchor
-- @return					a URL.
-----------------------------------------------------------------------------
function WSApp:make_url(node_name, action, params, anchor)
	if not node_name or node_name=="" then
		node_name = self.config.HOME_PAGE
	end

	local dirified = self:dirify(node_name) --wsapi.util.url_encode()

	local url

	-- first the node name
	url = self.config.NICE_URL..dirified

	-- then the action and HOME_PAGE
	if action and action~="show" then 
		url = url.."."..action
	elseif dirified==self.config.HOME_PAGE and #(params or {})==0 then
		url = self.config.HOME_PAGE_URL
	end

	-- then the parameters
	if params and next(params) then

		for k, v in pairs(params or {}) do
			if k~="p" then
				url = url.."&"..wsapi.util.url_encode(k).."="
								  ..wsapi.util.url_encode(v or "")
			end
		end
	end

	-- finally the anchor
	if anchor then
		url = url.."#"..anchor
	end

	return self:escape(url), node_name
end


-----------------------------------------------------------------------------
-- Makes a link from a table of parameters.
--
-- @param node_name		a node id.
-- @param action			action/command as a string.
-- @param params			query parameters
-- @param anchor			link anchor
-- @return					a URL.
-----------------------------------------------------------------------------
function WSApp:make_link(node_name, action, params, anchor, options)
	assert(node_name)
	options=options or {}
	-- check if we have a command attached to the node name
	if node_name:find("%.") then 
		node_name, action = node_name:match("(.+)%.(.+)")
	end
	local url = self:make_url(node_name, action, params, anchor)
	if options.mark_missing and not self.dal:GetNode(node_name) then
		return string.format('href="%s" class="no_such_node"', url)
	else
		return string.format('href="%s"', url, css_class)
	end
end

-----------------------------------------------------------------------------
-- Escapes a text for using in a textarea.
--
-- @param text			  text to be escaped.
-- @return					escaped text.
-----------------------------------------------------------------------------
function WSApp:escape(text)
	return zwsapi.util.escape(text)
end

-----------------------------------------------------------------------------
-- Returns a url of the user's profile node or nil
-----------------------------------------------------------------------------
function WSApp:make_link_to_user(user)
	if self.config.USE_USER_NODES and user then
		local prefix = self.config.USER_NODE_PREFIX or "people/"
		return self:make_link(prefix..user)
	end
end
function WSApp:format_time(timestamp, format, tzoffset, tzname)
	if type(timestamp) == "number" or timestamp:match("^[0-9]*$") then
		timestamp = os.date("!%Y-%m-%d %H:%M:%S", timestamp)
	end
	return zwsapi.util.format_time(timestamp,format, 
											  tzoffset or self.config.TIME_ZONE,
											  tzname or self.config.TIME_ZONE_NAME)
end